﻿using AutoMapper;
using CleanArchitecture.Core.Configuration;
using CleanArchitecture.Core.Handlers;
using CleanArchitecture.Core.Helper;
using CleanArchitecture.Core.Http;
using CleanArchitecture.Infrastructure.AutoMapper;
using CleanArchitecture.Infrastructure.Entities.Settings;
using CleanArchitecture.Infrastructure.Interfaces.Authentication;
using CleanArchitecture.Infrastructure.ModelBinding;
using CleanArchitecture.Infrastructure.Services;
using CleanArchitecture.Infrastructure.Services.Authentication;
using CleanArchitecture.Infrastructure.Services.SystemTasks;
using FluentValidation;
using FluentValidation.AspNetCore;
using Masuit.LuceneEFCore.SearchEngine;
using Masuit.LuceneEFCore.SearchEngine.Extensions;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace CleanArchitecture.Infrastructure
{
    /// <summary>
    /// Represents extensions of IServiceCollection
    /// </summary>
    /// <Author>
    ///  DavidLee on Nov.5 2019
    /// </Author>
    public static class StartupServiceSetup
	{
        /// <summary>
        /// Add services to the application and configure service provider
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        /// <param name="configuration">Configuration of the application</param>
        /// <returns>Configured service provider</returns>
		public static IServiceProvider ConfigureApplicationServices(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
        {
            //most of API providers require TLS 1.2 nowadays
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            CommonHelper.DefaultFileProvider = new NowaFileProvider(hostingEnvironment.WebRootPath, 
                                                                    hostingEnvironment.ContentRootPath ?? string.Empty);
            //add NowaConfig configuration parameters
            var nowaConfig = services.ConfigureStartupConfig<NowaConfig>(configuration.GetSection("Nowa"));
            //add hosting configuration parameters
            services.ConfigureStartupConfig<HostingConfig>(configuration.GetSection("Hosting"));
            //add accessor to HttpContext
            services.AddHttpContextAccessor();

            //DavidLee 2020/06/19 增加执行计划任务实例的全局单例注册，确保服务启动运行周期访问同一实例对象
            //services.AddSingleton(TaskManager.Instance);
            var taskManager = TaskManager.Create();
            //create and configure the engine
            var engine = EngineContext.Create();
            //engine.Initialize(services);
            var serviceProvider = engine.ConfigureServices(services, configuration, nowaConfig);

            //初使化并启动后台自动运行任务
            //TaskManager.Instance.Initialize();
            //TaskManager.Instance.Start();
            taskManager.Initialize();

            return serviceProvider;
        }

        /// <summary>
        /// Create, bind and register as service the specified configuration parameters 
        /// </summary>
        /// <typeparam name="TConfig">Configuration parameters</typeparam>
        /// <param name="services">Collection of service descriptors</param>
        /// <param name="configuration">Set of key/value application configuration properties</param>
        /// <returns>Instance of configuration parameters</returns>
        public static TConfig ConfigureStartupConfig<TConfig>(this IServiceCollection services, IConfiguration configuration) where TConfig : class, new()
        {
            if (services == null)
                throw new ArgumentNullException(nameof(services));

            if (configuration == null)
                throw new ArgumentNullException(nameof(configuration));

            //create instance of config
            var config = new TConfig();

            //bind it to the appropriate section of configuration
            configuration.Bind(config);

            //and register it as a service
            services.AddSingleton(config);

            return config;
        }

        /// <summary>
        /// Register HttpContextAccessor
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddHttpContextAccessor(this IServiceCollection services)
        {
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        }

        /// <summary>
        /// Add and configure EasyCaching service
        /// https://www.cnblogs.com/catcher1994/p/10806607.html
        /// office website:https://github.com/dotnetcore/EasyCaching
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddEasyCaching(this IServiceCollection services)
        {
            services.AddEasyCaching(option =>
            {
                //use memory cache
                option.UseInMemory("nowa_memory_cache_david");
            });
        }

        /// <summary>
        /// Adds services required for anti-forgery support
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddAntiForgery(this IServiceCollection services)
        {
            //override cookie name
            services.AddAntiforgery(options =>
            {
                options.Cookie.Name = $"{HttpDefineDefaults.Prefix}{HttpDefineDefaults.AntiforgeryCookie}";

                //whether to allow the use of anti-forgery cookies from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<SecuritySettings>().ForceSslForAllPages
                        ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });
        }

        /// <summary>
        /// Adds services required for application session state
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddHttpSession(this IServiceCollection services)
        {
            services.AddSession(options =>
            {
                options.Cookie.Name = $"{HttpDefineDefaults.Prefix}{HttpDefineDefaults.SessionCookie}";
                options.Cookie.HttpOnly = true;

                //whether to allow the use of session values from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            });
        }

        /// <summary>
        /// Add and configure default HTTP clients
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddCustomHttpClients(this IServiceCollection services)
        {
            //default client
            services.AddHttpClient(HttpDefineDefaults.DefaultHttpClient).WithProxy();
        }

        /// <summary>
        /// Add and configure MVC for the application
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        /// <returns>A builder for configuring MVC services</returns>
        public static IMvcBuilder AddCustomNetCoreMvc(this IServiceCollection services)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder => builder.WithOrigins(
                    new[] {
                        "http://localhost:57678",
                        "http://localhost:57679",
                        "http://localhost:8080",
                        "http://localhost:80",
                    })
                    .AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowCredentials()
                    .SetIsOriginAllowed((host) => true));
            });
            // 添加Signalr
            services.AddSignalR(config => {
                config.EnableDetailedErrors = true;
            });
            //add basic MVC feature
            //DavidLee 2019/11/11 在升级到ASP.NET Core 2.2后，会自动启用Endpoint路由。
            //see https://www.cnblogs.com/Weilence/p/10616567.html
            var mvcBuilder = services.AddMvc(option => option.EnableEndpointRouting = false);

            //DavidLee 2019/11/18: 
            // Mvc Routing config: url route in lowercase for readable and system limits(e.g: linux system)
            // and SEO 
            services.AddRouting(options => options.LowercaseUrls = true);
            //sets the default value of settings on MvcOptions to match the behavior of asp.net core mvc 3.0
            mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

            var nowaConfig = services.BuildServiceProvider().GetRequiredService<NowaConfig>();
            if (nowaConfig.UseSessionStateTempDataProvider)
            {
                //use session-based temp data provider
                mvcBuilder.AddSessionStateTempDataProvider();
            }
            else
            {
                //use cookie-based temp data provider
                mvcBuilder.AddCookieTempDataProvider(options =>
                {
                    options.Cookie.Name = $"{HttpDefineDefaults.Prefix}{HttpDefineDefaults.TempDataCookie}";

                    //whether to allow the use of cookies from SSL protected page on the other store pages which are not
                    options.Cookie.SecurePolicy = EngineContext.Current.Resolve<SecuritySettings>().ForceSslForAllPages
                        ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
                });
            }
            services.AddRazorPages();
            //MVC now serializes JSON with camel case names by default, use this code to avoid it
            //DavidLee 2019/11/8 dotNet Core 3.0 将NewtonsoftJson单独扩展，NopCommece框架中与.NetCore 2.x调用方法有出入
            mvcBuilder.AddNewtonsoftJson(options =>
            {
                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
                //options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
                options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";

            }); 

            // DavidLee 2019/11/16 
            // 参考nopCommerce框架并改造满足前端ajax post至后端action方法的数据-参数模型绑定
            //add custom display metadata provider
            //mvcBuilder.AddMvcOptions(options => options.ModelMetadataDetailsProviders.Add(new NopMetadataProvider()));
            //add custom model binder provider (to the top of the provider list)
            mvcBuilder.AddMvcOptions(options => {
                IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
                options.ModelBinderProviders.Insert(0, new MvcModelBinderProvider(options.InputFormatters, readerFactory));
            });

            //add fluent validation
            mvcBuilder.AddFluentValidation(configuration =>
            {
                //FluentValidation.Attributes 在更新依赖包后已经被弃用
                //configuration.ValidatorFactoryType = typeof(FluentValidatorFactory);
                ////implicit/automatic validation of child properties
                //configuration.ImplicitlyValidateChildProperties = true;
                //register all available validators from Nop assemblies
                var assemblies = mvcBuilder.PartManager.ApplicationParts
                    .OfType<AssemblyPart>()
                    .Where(part => part.Name.StartsWith("CleanArchitecture", StringComparison.InvariantCultureIgnoreCase))
                    .Select(part => part.Assembly);
                configuration.RegisterValidatorsFromAssemblies(assemblies);

                //implicit/automatic validation of child properties
                configuration.ImplicitlyValidateChildProperties = true;
            });

            return mvcBuilder;
        }

        /// <summary>
        /// Register custom RedirectResultExecutor
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddCustomNetCoreRedirectResultExecutor(this IServiceCollection services)
        {
            //we use custom redirect executor as a workaround to allow using non-ASCII characters in redirect URLs
            services.AddSingleton<IActionResultExecutor<RedirectResult>, CustomNetCoreRedirectResultExecutor>();
        }

        /// <summary>
        /// Register base object context
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddCustomObjectContext(this IServiceCollection services)
        {
            //https://github.com/dotnet/efcore/issues/9417 使用数据库连接池时不能在DbContext中设置UseQueryTrackingBehavior
            services.AddDbContextPool<AppDbObjectContext>(optionsBuilder =>
            {
                //optionsBuilder.UseSqlServerWithLazyLoading(services);
                optionsBuilder.UseDatabaseWithLazyLoading(services);
                optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll);
            });
            //Add SearchEngine
            services.AddSearchEngine<AppDbObjectContext>(new LuceneIndexerOptions()
            {
                Path = "LuceneDocument"
            });// 依赖注入搜索引擎，并配置索引库路径
        }

        /// <summary>
        /// Register and configure AutoMapper
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        /// <param name="typeFinder">Type finder</param>
        public static void AddAutoMapper(this IServiceCollection services)
        {
            var typeFinder = new WebAppTypeFinder();
            //find mapper configurations provided by other assemblies
            var mapperConfigurations = typeFinder.FindClassesOfType<IOrderedMapperProfile>();

            //create and sort instances of mapper configurations
            var instances = mapperConfigurations
                .Select(mapperConfiguration => (IOrderedMapperProfile)Activator.CreateInstance(mapperConfiguration))
                .OrderBy(mapperConfiguration => mapperConfiguration.Order);

            //create AutoMapper configuration
            var config = new MapperConfiguration(cfg =>
            {
                foreach (var instance in instances)
                {
                    cfg.AddProfile(instance.GetType());
                }
            });

            //register
            AutoMapperConfiguration.Init(config);
        }
        /// <summary>
        /// Adds data protection services
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddCustomDataProtection(this IServiceCollection services)
        {
            //check whether to persist data protection in Redis
            var nowaConfig = services.BuildServiceProvider().GetRequiredService<NowaConfig>();
            if (nowaConfig.RedisCachingEnabled && nowaConfig.PersistDataProtectionKeysToRedis)
            {
                //store keys in Redis
                //services.AddDataProtection().PersistKeysToRedis(() =>
                //{
                //    var redisConnectionWrapper = EngineContext.Current.Resolve<IRedisConnectionWrapper>();
                //    return redisConnectionWrapper.GetDatabase();
                //}, NopCachingDefaults.RedisDataProtectionKey);
                throw new NotImplementedException("Redis Not Supported yet.");
            }
            else
            {
                var dataProtectionKeysPath = CommonHelper.DefaultFileProvider.MapPath("~/App_Data/DataProtectionKeys");
                var dataProtectionKeysFolder = new System.IO.DirectoryInfo(dataProtectionKeysPath);
                //configure the data protection system to persist keys to the specified directory
                services.AddDataProtection().PersistKeysToFileSystem(dataProtectionKeysFolder);
            }
        }

        /// <summary>
        /// Adds authentication service
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddCustomAuthentication(this IServiceCollection services)
        {
            //set default authentication schemes
            //DavidLee 在 authentication 中间件上，区分各个handler的方法是指定不同的 authentication scheme，而不是通过 handler 的类型
            var authenticationBuilder = services.AddAuthentication(options =>
            {
                options.DefaultScheme = AuthenticationDefaultConfig.CookieAuthenticationScheme;
                options.DefaultSignInScheme = AuthenticationDefaultConfig.ExternalAuthenticationScheme;
            });

            //add main cookie authentication
            authenticationBuilder.AddCookie(AuthenticationDefaultConfig.CookieAuthenticationScheme, options =>
            {
                options.Cookie.Name = $"{HttpDefineDefaults.Prefix}{HttpDefineDefaults.AuthenticationCookie}";
                options.Cookie.HttpOnly = true;
                options.LoginPath = new PathString(StartupConfig.DefaultRoutePath.LoginPath);
                options.AccessDeniedPath = new PathString(StartupConfig.DefaultRoutePath.AccessDenied403);

                //whether to allow the use of authentication cookies from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<SecuritySettings>().ForceSslForAllPages
                    ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });

            //add external authentication
            authenticationBuilder.AddCookie(AuthenticationDefaultConfig.ExternalAuthenticationScheme, options =>
            {
                options.Cookie.Name = $"{HttpDefineDefaults.Prefix}{HttpDefineDefaults.ExternalAuthenticationCookie}";
                options.Cookie.HttpOnly = true;
                options.LoginPath = new PathString(StartupConfig.DefaultRoutePath.LoginPath);
                options.AccessDeniedPath = new PathString(StartupConfig.DefaultRoutePath.AccessDenied403);

                //whether to allow the use of authentication cookies from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<SecuritySettings>().ForceSslForAllPages
                    ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });

            //register and configure external authentication plugins now
            var typeFinder = new WebAppTypeFinder();
            var externalAuthConfigurations = typeFinder.FindClassesOfType<IExternalAuthenticationRegistrar>();
            var externalAuthInstances = externalAuthConfigurations
                .Select(x => (IExternalAuthenticationRegistrar)Activator.CreateInstance(x));

            foreach (var instance in externalAuthInstances)
                instance.Configure(authenticationBuilder);
        }
    }

    /// <summary>
    /// DavidLee 2020/09/01 更新了所有依赖包：
    /// using FluentValidation.Attributes; 已经被弃用
    /// Represents custom validator factory that looks for the attribute instance on the specified type in order to provide the validator instance.
    /// </summary>
    //public class FluentValidatorFactory : AttributedValidatorFactory
    //{
    //    /// <summary>
    //    /// Gets a validator for the appropriate type
    //    /// </summary>
    //    /// <param name="type">Type</param>
    //    /// <returns>Created IValidator instance; null if a validator cannot be created</returns>
    //    public override IValidator GetValidator(Type type)
    //    {
    //        if (type == null)
    //            return null;

    //        //get a custom attribute applied to a member of a type
    //        var validatorAttribute = (ValidatorAttribute)Attribute.GetCustomAttribute(type, typeof(ValidatorAttribute));
    //        if (validatorAttribute == null || validatorAttribute.ValidatorType == null)
    //            return null;

    //        //try to create instance of the validator
    //        var instance = EngineContext.Current.ResolveUnregistered(validatorAttribute.ValidatorType);

    //        return instance as IValidator;
    //    }
    //}

    /// <summary>
    /// Represents custom overridden redirect result executor
    /// </summary>
    public class CustomNetCoreRedirectResultExecutor : RedirectResultExecutor
    {
        #region Fields

        private readonly SecuritySettings _securitySettings;

        #endregion

        #region Ctor

        public CustomNetCoreRedirectResultExecutor(ILoggerFactory loggerFactory,
            IUrlHelperFactory urlHelperFactory,
            SecuritySettings securitySettings) : base(loggerFactory, urlHelperFactory)
        {
            this._securitySettings = securitySettings;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Execute passed redirect result
        /// </summary>
        /// <param name="context">Action context</param>
        /// <param name="result">Redirect result</param>
        /// <returns>Task that represents the asynchronous operation</returns>
        public override Task ExecuteAsync(ActionContext context, RedirectResult result)
        {
            if (result == null)
                throw new ArgumentNullException(nameof(result));

            if (_securitySettings.AllowNonAsciiCharactersInHeaders)
            {
                //passed redirect URL may contain non-ASCII characters, that are not allowed now (see https://github.com/aspnet/KestrelHttpServer/issues/1144)
                //so we force to encode this URL before processing
                result.Url = Uri.EscapeUriString(WebUtility.UrlDecode(result.Url));
            }

            return base.ExecuteAsync(context, result);
        }

        #endregion
    }

}
